/*
 * MIT License
 * 
 * Copyright (c) 2024-2025 milkpotatoes
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include <uv.h>

#include "napi_utils.h"
#include "utils.h"


#include "include/js_console.h"

#define COLOR_AUTO ""
#define COLOR_DEFAULT "\x1b[0m"
#define COLOR_STRING COLOR_DEFAULT
#define COLOR_NUMBER "\x1b[33m"
#define COLOR_BIGINT COLOR_NUMBER
#define COLOR_BOOLEAN COLOR_NUMBER
#define COLOR_FUNCTION "\x1b[36m"
#define COLOR_SPECIAL_TAG COLOR_FUNCTION
#define COLOR_UNDEFINED "\x1b[2m"
#define COLOR_NULL "\x1b[1m"
#define COLOR_SYMBOL "\x1b[32m"
#define COLOR_DATE "\x1b[35m"
#define COLOR_OBJECT COLOR_DEFAULT
#define COLOR_OBJECT_STRING COLOR_SYMBOL

std::string ValueToString(napi_env env, napi_value value, bool colorful = true);
std::string ObjectToString(napi_env env, napi_value object, bool &useReturn, uint16_t indent = 0, bool colorful = true);

class VisitedStorage : public std::enable_shared_from_this<VisitedStorage>
{
public:
    static inline std::shared_ptr<VisitedStorage> New(napi_env env, napi_value value)
    {
        for (auto &it : GetMarkedVector())
        {
            if (*it == value)
                return it;
        }
        auto that = std::make_shared<VisitedStorage>(env, value);
        GetMarkedVector().insert(that);
        return that;
    }

    inline VisitedStorage(napi_env env, napi_value value) : env_(env), value_(value)
    {
    }
    inline bool operator==(napi_value rhs)
    {
        bool ret = false;
        napi_strict_equals(env_, value_, rhs, &ret);
        if (!ret)
            return ret;
        if (id_ == 0)
            id_ = ++GetCurrentId();
        return ret;
    }

    uint32_t operator*() const
    {
        return id_;
    }

    void SetCircular()
    {
        circular_ = true;
    }
    bool IsCircular()
    {
        return circular_;
    }

    void ResetCircular()
    {
        circular_ = false;
    }

    static std::vector<std::shared_ptr<VisitedStorage>> &GetVisitedVector()
    {
        thread_local std::vector<std::shared_ptr<VisitedStorage>> visitedVector{};
        return visitedVector;
    }
    static void Reset()
    {
        GetVisitedVector().clear();
        GetMarkedVector().clear();
        GetCurrentId() = 0;
    }

private:
    static std::unordered_set<std::shared_ptr<VisitedStorage>> &GetMarkedVector()
    {
        thread_local std::unordered_set<std::shared_ptr<VisitedStorage>> markedVector{};
        return markedVector;
    }
    static uint32_t &GetCurrentId()
    {
        thread_local uint32_t id;
        return id;
    }

    const napi_env env_{nullptr};
    const napi_value value_{nullptr};
    bool circular_{false};
    uint32_t id_{0};
};

std::string QuotationMarks(std::string &str)
{
    std::string quot = "'";
    utils::Replace(str, std::string("\\"), std::string("\\\\"));
    utils::Replace(str, std::string("\n"), std::string("\\n"));
    if (str.find("'"))
        quot = "\"";
    if (str.find("\""))
        quot = "`";
    if (str.find("`"))
        quot = "";
    if (quot != "")
    {
        return quot;
    }
    else
    {
        quot = "'";
        utils::Replace(str, quot, std::string("\\'"));
    }
    return quot;
}

bool IsIdentifier(std::string str)
{
    constexpr const char *commonKey = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
    constexpr const char *numberKey = "0123456789";
    if (str.empty() || strchr(numberKey, str[0]) != nullptr)
        return false;
    for (char c : str)
    {
        if (strchr(commonKey, c) == nullptr)
            return false;
    }
    return true;
}

std::string MakeLeading(uint32_t size, const char *chr = " ")
{
    std::string leading = "";
    for (uint16_t i = 0; i < size; i++)
    {
        leading += chr;
    }
    return leading;
}

std::string PropertyToString(napi_env env, napi_value value, bool &useReturn, uint16_t indent = 0, bool colorful = true)
{
    napi_valuetype type = utils::TypeOf(env, value);
    std::string result = "";
    switch (type)
    {
    case napi_string: {
        std::string str = ValueToString(env, value, false);
        std::string qout = QuotationMarks(str);
        if (colorful)
            return std::string(COLOR_OBJECT_STRING + qout + str + qout + COLOR_DEFAULT);
        else
            return std::string(qout + str + qout);
    }
    case napi_object:
        return ObjectToString(env, value, useReturn, indent + 1, colorful);
    default: {
        result += ValueToString(env, value, colorful);
        return result;
    }
    }
}

std::string DumpObject(napi_env env, napi_value object, bool &useReturn, uint16_t indent, bool colorful)
{
    std::vector<std::string> results;

    bool useRet = useReturn;

    results.emplace_back("{");

    std::string str = "";
    bool foundHole = false;
    napi_value keys = nullptr;
    napi_get_all_property_names(env, object, napi_key_own_only, napi_key_enumerable, napi_key_keep_numbers, &keys);
    uint32_t len = 0;
    uint32_t strLen = 0;
    napi_get_array_length(env, keys, &len);
    for (uint32_t i = 0, lastHole = 0; i < len; i++)
    {
        if (!utils::HasElement(env, keys, i)) continue;
        napi_value key = utils::GetElement(env, keys, i);
        napi_value val = utils::GetProperty(env, object, key);

        if (utils::IsSymbol(env, key))
            str = "[" + ValueToString(env, key, colorful) + "]: ";
        else
        {
            std::string kStr = ValueToString(env, key, false);
            std::string q = "";
            if (!IsIdentifier(kStr))
                q = QuotationMarks(kStr);

            if (colorful && !q.empty())
                str = COLOR_OBJECT_STRING + q + kStr + q + COLOR_DEFAULT ": ";
            else
                str = q + kStr + q + ": ";
        }

        str += PropertyToString(env, val, useReturn, indent, colorful);
        if (i < len - 1)
            str += ",";
        strLen += str.size();
        if (strLen > 80)
            useRet = true;
        results.emplace_back(str);
    }

    results.emplace_back("}");
    std::string result = results[0];
    std::string leading = useRet ? MakeLeading((indent + 1) * 2) : "";
    for (uint32_t i = 1; i < results.size() - 1; i++)
    {
        result += useRet ? "\n" : " ";
        result += leading;
        result += results[i];
    }
    result += useRet ? "\n" : " ";
    result += (useRet ? MakeLeading(indent * 2) : "") + results[results.size() - 1];
    return result;
}

std::string DumpArray(napi_env env, napi_value object, bool &useReturn, uint16_t indent, bool colorful)
{
    std::vector<std::string> results;

    results.emplace_back("[");

    std::string str = "";
    bool foundHole = false;
    uint32_t len = 0;
    napi_get_array_length(env, object, &len);
    uint32_t lastHole = 0;
    for (uint32_t i = 0; i < len; i++)
    {
        if (!utils::HasElement(env, object, i))
        {
            if (!foundHole)
            {
                foundHole = true;
                lastHole = i;
                if (colorful)
                    str = COLOR_UNDEFINED "<";
                else
                    str = "<";
            }
        }
        else
        {
            napi_value val = utils::GetElement(env, object, i);
            if (foundHole)
            {
                str += std::to_string(i - lastHole);
                str += " empty item>";
                if (colorful)
                    str += COLOR_DEFAULT;

                foundHole = false;
                str += ",";
                results.emplace_back(str);
            }

            str = PropertyToString(env, val, useReturn, indent, colorful);
            if (i < len - 1)
                str += ",";
            if (!useReturn && str.size() > 36)
                useReturn = true;
            results.emplace_back(str);
        }
    }

    if (foundHole)
    {
        str += std::to_string(len - lastHole);
        str += " empty item>";
        if (colorful)
            str += COLOR_DEFAULT;

        foundHole = false;
        results.emplace_back(str);
    }

    results.emplace_back("]");
    std::string result = results[0];
    std::string leading = useReturn ? MakeLeading((indent + 1) * 2) : "";
    for (uint32_t i = 1; i < results.size() - 1; i++)
    {
        result += useReturn ? "\n" : " ";
        result += leading;
        result += results[i];
    }
    result += useReturn ? "\n" : " ";
    result += (useReturn > 0 ? MakeLeading(indent * 2) : "") + results[results.size() - 1];
    return result;
}

std::string ObjectToString(napi_env env, napi_value object, bool &useReturn, uint16_t indent, bool colorful)
{
    useReturn = false;
    for (auto it : VisitedStorage::GetVisitedVector())
    {
        if (*it == object)
        {
            it->SetCircular();
            std::string result = "[Circular *";
            result += std::to_string(**it) + "]";
            if (colorful)
                result = COLOR_SPECIAL_TAG + result + COLOR_DEFAULT;
            return result;
        }
    }

    std::shared_ptr<VisitedStorage> visitor = VisitedStorage::New(env, object);
    VisitedStorage::GetVisitedVector().emplace_back(visitor);
    bool isArray = utils::IsArray(env, object);
    std::string result;
    if (utils::IsArray(env, object))
        result = DumpArray(env, object, useReturn, indent, colorful);
    else
        result = DumpObject(env, object, useReturn, indent, colorful);
    if (visitor->IsCircular())
    {
        std::string tag = "<ref *" + std::to_string(**visitor) + "> ";
        if (colorful)
            result = COLOR_SPECIAL_TAG + tag + COLOR_DEFAULT + result;
        else
            result = tag + result;
    }
    VisitedStorage::GetVisitedVector().pop_back();
    return result;
}

std::string ErrorToString(napi_env env, napi_value error)
{
    std::string result = "";
    napi_value constructor = utils::GetProperty(env, error, "constructor");
    napi_value consName = utils::GetProperty(env, constructor, "name");
    napi_value msg = utils::GetProperty(env, error, "message");
    napi_value stack = utils::GetProperty(env, error, "stack");
    result += utils::GetValueStringUtf8(env, consName) + ": ";
    result += utils::GetValueStringUtf8(env, msg) + "\n";
    result += utils::GetValueStringUtf8(env, stack);
    return result;
}

std::string BuiltinToString(napi_env env, napi_value value)
{
    napi_value toStr = utils::GetProperty(env, value, "toString");
    napi_value str = nullptr;
    napi_status status = napi_call_function(env, value, toStr, 0, nullptr, &str);
    return utils::GetValueStringUtf8(env, str);
}

std::string ValueToString(napi_env env, napi_value value, bool colorful)
{
    std::string result = "";
    bool coerce = true;
    napi_value jsStr = nullptr;
    napi_valuetype type = utils::TypeOf(env, value);
    switch (type)
    {
    case napi_string:
        if (colorful)
            result += COLOR_STRING;
        result += utils::GetValueStringUtf8(env, value);
        break;
    case napi_object: {
        if (colorful)
            result += COLOR_OBJECT;
        if (utils::IsError(env, value))
        {
            result += ErrorToString(env, value);
        }
        else if (utils::IsDate(env, value))
        {
            if (colorful)
                result += COLOR_DATE;
            result += BuiltinToString(env, value);
        }
        else
        {
            bool useReturn = false;
            napi_value cons = utils::GetProperty(env, value, "constructor");
            std::string consName = utils::GetValueStringUtf8(env, utils::GetProperty(env, cons, "name"));
            if (consName != "Object" && consName != "Array")
            {
                result += consName;
                result += " ";
            }
            result += ObjectToString(env, value, useReturn, 0, colorful);
        }
        break;
    }
    case napi_function: {
        if (colorful)
            result += COLOR_FUNCTION;
        result += "[Function ";
        napi_value jsName = utils::GetProperty(env, value, "name");
        std::string name = utils::GetValueStringUtf8(env, jsName);
        if (name == "")
            result += "(anonymous)";
        else
            result += name;
        result += "]";
        break;
    }
    case napi_undefined:
        if (colorful)
            result += COLOR_UNDEFINED;
        result += "undefined";
        break;
    case napi_external:
        if (colorful)
            result += COLOR_OBJECT;
        result += "External {}";
        break;
    case napi_null:
        if (colorful)
            result += COLOR_NULL;
        result += "null";
        break;
    case napi_number: {
        if (colorful)
            result += COLOR_NUMBER;
        napi_value strNum = utils::CoerceToString(env, value);
        result += utils::GetValueStringUtf8(env, strNum);
        break;
    }
    case napi_boolean:
        if (colorful)
            result += COLOR_BOOLEAN;
        if (utils::GetValueBool(env, value))
            result += "true";
        else
            result += "false";
        break;
    case napi_bigint: {
        if (colorful)
            result += COLOR_BIGINT;
        size_t counts = 0;
        napi_get_value_bigint_words(env, value, nullptr, &counts, nullptr);
        int sign = 0;
        std::vector<uint64_t> words(counts);
        napi_get_value_bigint_words(env, value, &sign, &counts, words.data());
        if (sign)
            result += "-";
        for (size_t i = 0; i < counts; i++)
        {
            result += std::to_string(words[i]);
        }
        result += "n";
        break;
    }
    case napi_symbol: {
        if (colorful)
            result += COLOR_SYMBOL;
        napi_value symbolObj = utils::CoerceToObject(env, value);
        result += BuiltinToString(env, symbolObj);
        break;
    }
    default:
        break;
    }
    if (colorful)
        result += COLOR_DEFAULT;
    return result;
}

std::string Console::ParseLogParam(napi_env env, napi_callback_info info_, bool isError)
{
    utils::NapiCallbackInfo info(env, info_);
    if (info.GetArgc() == 0)
        return "";
    std::vector<std::string> result;
    for (uint32_t i = 0; i < info.GetArgc(); i++)
    {
        VisitedStorage::Reset();
        std::string val = ValueToString(env, info[i], isError ? utils::IsStdErr() : utils::IsStdOut());
        result.emplace_back(val);
    }
    if (result.size() == 0)
        return "";
    std::string formatted = result[0];
    for (uint32_t i = 1; i < result.size(); i++)
    {
        formatted += " " + result[i];
    }
    return formatted;
};

napi_value Console::InnerTimeInNs(napi_env env, napi_callback_info)
{
    uint64_t nsTime = uv_hrtime();
    napi_value result = nullptr;
    napi_create_bigint_uint64(env, nsTime, &result);
    return result;
}

void Console::BindToGlobal(napi_env env, napi_value global)
{
    napi_value console = nullptr;
    napi_create_object(env, &console);
    napi_property_descriptor consoleProp[] = {
        {"log", nullptr, ConsoleInfo, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"debug", nullptr, ConsoleInfo, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"info", nullptr, ConsoleInfo, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"warn", nullptr, ConsoleError, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"error", nullptr, ConsoleError, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerTimeInNs", nullptr, InnerTimeInNs, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
    };
    napi_define_properties(env, console, sizeof(consoleProp) / sizeof(consoleProp[0]), consoleProp);
    utils::SetUnenumerableProperty(env, global, "console", console);

    return;
};
